using System.Buffers;
using System.Net;
using System.Text;
using SuperSocket.Channel;
using SuperSocket.ProtoBase;

namespace SuperSocket.Client.Proxy
{
    /// <summary>
    /// https://tools.ietf.org/html/rfc1928
    /// https://en.wikipedia.org/wiki/SOCKS
    /// </summary>
    public sealed class Socks5Connector : ProxyConnectorBase<Socks5Pack>
    {
        public Socks5Connector(EndPoint proxyEndPoint)
            : base(proxyEndPoint)
        {
        }

        public Socks5Connector(EndPoint proxyEndPoint, IConnector nextConnector)
            : base(proxyEndPoint, nextConnector)
        {
        }

        public Socks5Connector(EndPoint proxyEndPoint, string username, string password, IConnector nextConnector) :
            base(proxyEndPoint, username, password, nextConnector)
        {
        }

        protected override IChannel<Socks5Pack> CreateChannel(ConnectState state, ChannelOptions channelOptions)
            => state.CreateChannel(new Socks5AuthPipelineFilter(), channelOptions);

        protected override async ValueTask<bool> ConnectProxyAsync(EndPoint remoteEndPoint,
            IChannel<Socks5Pack> channel, IAsyncEnumerator<Socks5Pack> packStream, CancellationToken cancellationToken)
        {
            cancellationToken.ThrowIfCancellationRequested();
            
            var request = new Socks5Request(channel, packStream, remoteEndPoint, Username, Password);

            var response = await request.SendHandshakeAsync();
            
            if (!HandleResponse(response, Socket5ResponseType.Handshake, out var errorMessage))
                throw new Exception(errorMessage);

            if (response.Status == 0x02) // need pass auth
            {
                response = await request.SendAuthenticateAsync();

                if (!HandleResponse(response, Socket5ResponseType.AuthUserName, out errorMessage))
                    throw new Exception(errorMessage);
            }

            response = await request.SendEndPointAsync();

            if (!HandleResponse(response, Socket5ResponseType.AuthEndPoint, out errorMessage))
                throw new Exception(errorMessage);

            return true;
        }

        private bool HandleResponse(Socks5Pack response, Socket5ResponseType responseType, out string errorMessage)
        {
            errorMessage = null;

            if (responseType == Socket5ResponseType.Handshake)
            {
                if (response.Status != 0x00 && response.Status != 0x02)
                {
                    errorMessage = $"failed to connect to proxy , protocol violation";
                    return false;
                }
            }
            else if (responseType == Socket5ResponseType.AuthUserName)
            {
                if (response.Status != 0x00)
                {
                    errorMessage = $"failed to connect to proxy ,  username/password combination rejected";
                    return false;
                }
            }
            else
            {
                if (response.Status != 0x00)
                {
                    errorMessage = response.Status switch
                    {
                        (0x02) => "connection not allowed by ruleset",
                        (0x03) => "network unreachable",
                        (0x04) => "host unreachable",
                        (0x05) => "connection refused by destination host",
                        (0x06) => "TTL expired",
                        (0x07) => "command not supported / protocol error",
                        (0x08) => "address type not supported",
                        _ => "general failure",
                    };
                    errorMessage = $"failed to connect to proxy ,  {errorMessage}";
                    return false;
                }
            }

            return true;
        }
    }

    enum Socket5ResponseType
    {
        Handshake,

        AuthUserName,

        AuthEndPoint,
    }

    public class Socks5Address
    {
        public IPAddress IPAddress { get; set; }

        public string DomainName { get; set; }
    }

    public class Socks5Pack
    {
        public byte Version { get; set; }

        public byte Status { get; set; }

        public byte Reserve { get; set; }

        public Socks5Address DestAddr { get; set; }

        public short DestPort { get; set; }
    }

    public sealed class Socks5AuthPipelineFilter : FixedSizePipelineFilter<Socks5Pack>
    {
        private readonly int _authStep;

        public Socks5AuthPipelineFilter(int authStep = 0)
            : base(2)
        {
            _authStep = authStep;
        }

        protected override Socks5Pack DecodePackage(ref ReadOnlySequence<byte> buffer)
        {
            var reader = new SequenceReader<byte>(buffer);
            reader.TryRead(out var version);
            reader.TryRead(out var status);

            if (_authStep == 0)
                NextFilter = new Socks5AuthPipelineFilter(1);
            else
                NextFilter = new Socks5AddressPipelineFilter();

            return new Socks5Pack
            {
                Version = version,
                Status = status
            };
        }
    }

    public sealed class Socks5AddressPipelineFilter : FixedHeaderPipelineFilter<Socks5Pack>
    {
        public Socks5AddressPipelineFilter()
            : base(5)
        {
        }

        protected override Socks5Pack DecodePackage(ref ReadOnlySequence<byte> buffer)
        {
            var reader = new SequenceReader<byte>(buffer);
            reader.TryRead(out var version);
            reader.TryRead(out var status);
            reader.TryRead(out var reserve);
            reader.TryRead(out var addressType);

            var address = new Socks5Address();

            if (addressType == 0x01)
            {
                const int addrLen = 4;
                address.IPAddress = new IPAddress(reader.Sequence.Slice(reader.Consumed, addrLen).ToArray());
                reader.Advance(addrLen);
            }
            else if (addressType == 0x04)
            {
                const int addrLen = 16;
                address.IPAddress = new IPAddress(reader.Sequence.Slice(reader.Consumed, addrLen).ToArray());
                reader.Advance(addrLen);
            }
            else if (addressType == 0x03)
            {
                reader.TryRead(out byte addrLen);
                var seq = reader.Sequence.Slice(reader.Consumed, addrLen);
                address.DomainName = seq.GetString(Encoding.ASCII);
                reader.Advance(addrLen);
            }
            else
            {
                throw new Exception($"Unsupported addressType: {addressType}");
            }

            reader.TryReadBigEndian(out ushort port);

            return new Socks5Pack
            {
                Version = version,
                Status = status,
                Reserve = reserve,
                DestAddr = address,
                DestPort = (short)port
            };
        }

        protected override int GetBodyLengthFromHeader(ref ReadOnlySequence<byte> buffer)
        {
            var reader = new SequenceReader<byte>(buffer);
            reader.Advance(3);
            reader.TryRead(out var addressType);

            if (addressType == 0x01)
                return 6 - 1;

            if (addressType == 0x04)
                return 18 - 1;

            if (addressType == 0x03)
            {
                reader.TryRead(out byte domainLen);
                return domainLen + 2;
            }

            throw new Exception($"Unsupported addressType: {addressType}");
        }
    }
}